package javango.contrib.hibernate;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javango.contrib.i18n.I18NProvider;
import javango.db.ManagerException;
import javango.db.QuerySet;

import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.logging.LogFactory;
import org.hibernate.Criteria;
import org.hibernate.HibernateException;
import org.hibernate.ScrollableResults;
import org.hibernate.criterion.DetachedCriteria;

import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;

public class HibernateQuerySet<T> implements QuerySet<T>, Iterable<T> {

	protected Map<String, Object> valueFilters = new LinkedHashMap<String, Object>();
	protected Map<String, String> propertyFilters = new LinkedHashMap<String, String>();
	protected List<String> orderbys = new ArrayList<String>(); 
	
	protected Integer first;
	protected Integer last;
	protected Class<? extends T> clazz;
	
	@Inject
	protected I18NProvider i18n;
	private String _(String format, Object... args) {
		if (i18n == null) {
			return String.format(format, args);
		}
		return i18n.getText(format, args);
	}
		
	// cached local copy of my criteria,  as I am immutable this can be cached once created
	private Criteria _criteria;
	
	HibernateQuerySetHelper qsHelper;

	
	// cached local copy of my count,  as I am immutable this can be cached once known.
	// subsequent calls to count() will always return the same value even if the underlying db changes.
	// i guess this could cause strangeness if the db was updated.  oh well DDN
	Long _count;
	
	
	HibernateUtil hibernateUtil;
	
	
	protected HibernateQuerySet<T> newInstance() throws ManagerException {
		return new HibernateQuerySet<T>(hibernateUtil, clazz);
	}
	
	@Override
	public QuerySet<T> clone() {
		try {
			return newInstance().cloneFrom(this);
		} catch (ManagerException e) {
			LogFactory.getLog(HibernateQuerySet.class).error(e,e);
		}
		return null;
	}
	
	// clone any properties from this object to the other object.  Returns this
	protected HibernateQuerySet<T> cloneFrom(HibernateQuerySet<T> other) throws ManagerException {
		this.valueFilters.putAll(other.valueFilters);
		this.propertyFilters.putAll(other.propertyFilters);
		this.orderbys.addAll(other.orderbys);
		this.first = other.first;
		this.last = other.last;
		return this;
	}
	
	@AssistedInject
	public HibernateQuerySet(HibernateUtil hibernateUtil, @Assisted Class<? extends T> clazz) throws ManagerException {
		super();
		this.hibernateUtil = hibernateUtil;
		this.clazz = clazz;
	}

	public QuerySet<T> filter(Map<String, ?> params, boolean ignoreNulls) throws ManagerException {
		return mapFilter(params, ignoreNulls);
	}
	
	public QuerySet<T> mapFilter(Map<String, ?> params, boolean ignoreNulls) throws ManagerException {
		HibernateQuerySet<T> other = newInstance().cloneFrom(this);
		// ignore null values,  this will change in .34
		// other.filters.putAll(params);
		for(Entry<String, ?> entry : params.entrySet()) {
			if (!ignoreNulls || entry.getValue() != null) other.valueFilters.put(entry.getKey(), entry.getValue());
		}
		return other;
	}

	public QuerySet<T> filter(Map<String, ?> params) throws ManagerException {
		return mapFilter(params,true);
	}
	
	public QuerySet<T> mapFilter(Map<String, ?> params) throws ManagerException {
		return mapFilter(params, true);
	}
	
	public QuerySet<T> filter(Object params) throws ManagerException {
		if (params instanceof Map) {
			return mapFilter((Map)params);
		}
		HibernateQuerySet<T> other = newInstance().cloneFrom(this);

		try {
			Class c = params.getClass();
			java.lang.reflect.Field[] fields = c.getDeclaredFields();
			for (int i=0; i<fields.length; i++) {
				String fieldName = fields[i].getName();
				Object value = PropertyUtils.getProperty(params, fieldName);				
				if (value != null) other.valueFilters.put(fieldName, value);
			}
		} catch (IllegalAccessException e) {
			LogFactory.getLog(HibernateQuerySet.class).error(e,e);
			throw new ManagerException(e);
		} catch (InvocationTargetException e) {
			LogFactory.getLog(HibernateQuerySet.class).error(e,e);
			throw new ManagerException(e);
		} catch (NoSuchMethodException e) {
			LogFactory.getLog(HibernateQuerySet.class).error(e,e);
			throw new ManagerException(e);
		}	
		return other;
	}

	public QuerySet<T> filter(String property, Object value) throws ManagerException {
		HibernateQuerySet<T> other = newInstance().cloneFrom(this);
		other.valueFilters.put(property, value);
		return other;
	}

	public QuerySet<T> filterByProperty(String propertyA, String propertyB) throws ManagerException {
		HibernateQuerySet<T> other = newInstance().cloneFrom(this);
		other.propertyFilters.put(propertyA, propertyB);
		return other;
	}
	
	public QuerySet<T> limit(int first, int last) throws ManagerException {
		HibernateQuerySet<T> other = newInstance().cloneFrom(this);
		other.first = first;
		other.last = last;
		return other;
	}
	
	public T get() throws ManagerException {
		try {
			return (T)getCriteria().uniqueResult();
		} catch (HibernateException e) {
			LogFactory.getLog(HibernateQuerySet.class).error(_("Hibernate Exception 'get()' while running '%s'", this.toString()));
			throw new ManagerException(e);
		}
	}

	public List<T> list() throws ManagerException {
		try {
			return getCriteria().list();
		} catch (HibernateException e) {			
			LogFactory.getLog(HibernateQuerySet.class).error(_("Hibernate Exception 'list()' while running '%s'", this.toString()));
			LogFactory.getLog(HibernateQuerySet.class).error(e,e);
			LogFactory.getLog(HibernateQuerySet.class).error(e,e.getCause());
			throw new ManagerException("Unable to run query");
		}
	}

	public static class QuerySetIterator<T> implements Iterator<T> {
		public QuerySetIterator(ScrollableResults scrollableResult) {
			this.scrollableResult = scrollableResult;
		}

		private ScrollableResults scrollableResult = null;
		private Object[] nextValue; // for some reason isLast returns false on empty lists..
		
		public boolean hasNext() {
			if (nextValue != null) {
				return true;
			}
			
			boolean found = scrollableResult.next();
			if (!found) return false;
			
			
			nextValue = scrollableResult.get();
			return nextValue != null;
		}

		@SuppressWarnings("unchecked")
		public T next() {
			if (nextValue == null || nextValue.length == 0) {
				return null;
			}
			
			T v = (T)nextValue[0];
			nextValue = null;
			return v;
		}

		public void remove() {
			throw new UnsupportedOperationException("remove not supported");      			
		}

	}
	public Iterator<T> iterator() {
		try {
			return new QuerySetIterator<T>(getCriteria().scroll());
		} catch (ManagerException e) {
			LogFactory.getLog(HibernateQuerySet.class).error(_("Hibernate Exception on 'iterator()' while running '%s'", this.toString()));
			LogFactory.getLog(HibernateQuerySet.class).error(e,e);
			throw new RuntimeException(e);
		}
	}

	public QuerySet<T> orderBy(String... orderbys) throws ManagerException {
		HibernateQuerySet<T> other = newInstance().cloneFrom(this);
		for(String order : orderbys) {
			other.orderbys.add(order);
		}
		return other;
	}

//	public QuerySet<T> or(QuerySet<T> _orOther) throws ManagerException {
//		if (_orOther instanceof HibernateQuerySet) {
//			HibernateQuerySet<T> orOther = (HibernateQuerySet<T>)_orOther;
//			if (orOther.clazz != this.clazz) {
//				throw new ManagerException("ORs only supported for the same model");
//			}
//			HibernateQuerySet<T> other = new HibernateQuerySet<T>(this);
//			other.ors = new ArrayList<HibernateQuerySet>();
//			other.ors.add(this);
//			other.ors.add(orOther);
//			return other;
//		}
//		throw new ManagerException("ORs only supported with HibernateQuerySets");
//	}
	
	// impelemtation specific functions	
	public long count() throws ManagerException {
		if (_count == null) {
			try {
				_count = new Long(hibernateUtil.doCount(getRawCriteria().getExecutableCriteria(hibernateUtil.getSession())));
			} catch (HibernateException e) {
				LogFactory.getLog(HibernateQuerySet.class).error(_("Hibernate Exception while running %s", this.toString()));
				throw new ManagerException(e);
			}
		}
		return _count;
	}

	/**
	 * Returns a Criteria prior to any limit or orders being applied.
	 * 
	 * @return
	 * @throws ManagerException
	 */
	public DetachedCriteria getRawCriteria() throws ManagerException {
		DetachedCriteria dc = DetachedCriteria.forClass(clazz);
		qsHelper = new HibernateQuerySetHelper(hibernateUtil, clazz, dc);
		qsHelper.updateCriteria(valueFilters);
		qsHelper.updateCriteriaFromPropertyMap(propertyFilters);
		
		return dc;
	}
	
	/**
	 * Returns a hibernate criteria representing this queryset.
	 * 
	 * @return
	 */
	public org.hibernate.Criteria getCriteria() throws ManagerException {
		if (_criteria != null) return _criteria;

		_criteria = getRawCriteria().getExecutableCriteria(hibernateUtil.getSession());
		
		if (this.first != null) {
			_criteria.setFirstResult(first < 0 ? 0 : first);
			if (this.last != null) {
				int count = this.last - this.first;
				_criteria.setMaxResults(count > 0 ? count : 1);
			}
		} else if (this.last != null) {
			_criteria.setMaxResults(this.last > 0 ? this.last : 1);
		}
		for (String s : orderbys) {
			qsHelper.addOrderBy(s);
		}
		
		return _criteria;
	}

	@Override
	public String toString() {
		return "HibernateQuerySet [valueFilters=" + valueFilters.toString()
				+ ", propertyFilters=" + propertyFilters.toString() + ", orderbys="
				+ orderbys + ", first=" + first + ", last=" + last + "]";
	}

	public Class<? extends T> getType() {
		return this.clazz;
	}
//
//	void getPropertyInformation(String path) {
//		String[] parts = path.split("__");
//		
//		QuerySet.Lookup lookupType = QuerySet.Lookup.EQ; // DEFAULT
//		if (parts.length > 1) {
//			String match = parts[parts.length-1];
//			for(QuerySet.Lookup lookup : QuerySet.Lookup.values()) {
//				if (lookup.equals(match)) {
//					lookupType = lookup;
//					break;
//				}
//			}
//		}
//		
//		
//	}
}
